﻿/*
线程的概念和控制
	一、线程  概念
	二、资源划分（虚拟到物理，页表，页表概念，部分内存管理）
	三、线程
	四、线程周边相关

一、线程
	进程 = 内核数据结构 + 代码和数据（执行流）    要占据内存资源   CPU资源
  ·线程：是进程内部的一个执行分支（执行流）

内核和资源：
	进程：承担分配系统资源的基本实体（每一次申请都要操作系统的一大批资源  task_struct  页表  mm_struct  物理内存）
进程进程之间互相不影响，都有着不一样的资源，都要申请（进程的申请是为了线程分配）
	线程：CPU调度的基本单位

	进程访问的大部分资源，都是通过地址空间访问的！（地址空间是进程看到资源的窗口）
	一个进程拥有多少资源 ---> 本质是进程能通过地址空间这个窗口看到多少资源

	但是如果说想要多个进程用同一个地址空间，不要新开页表等，要所有的进程直系那个同一个地址空间，所有进程就会注定
看到同一份资源，进程创建不要新的mm_struct，不要新的页表，只开结构体task_struct,然后把所有的结构体，指向同一个地址
空间，未来就看到同一个系统资源，将资源再分配给不同的的task_struct,也就可以用进程模仿出来线程了！
	这么一个个task_struct ---> 就是类似线程，划分地址空间（本质是在划分虚拟地址范围，就可以经过页表的映射和物理
地址转化就能访问到内存的不用区）


结论：1.Linux线程可以使用进程来模拟
	  2.对资源的划分，本质是对地址空间虚拟地址范围的划分！  
		虚拟地址就是资源的代表！
	  3.代码区划分：函数的本质是虚拟地址(磁盘上叫：逻辑地址)空间的集合，就是让线程未来执行ELF程序的不同的函数
	即可！     
	代码由函数构成，函数都有入口地址，函数本质是代码块，代码块每一行都有地址
	  4.线程 VS 进程
	之前的进程--->单进程（一种特殊情况）   以前的进程：内部只有一个线程的进程！
	  5.Linux的线程，就是轻量级进程！或者用轻量级进程模拟实现的！


为什么这样设计？
答案：复用之前的task_struct，用进程模拟线程，包括内核代码也会复用，这会让代码更为健壮，维护性更好！成本会更低！
	  代码的生命周期会更长！

其他的平台，譬如：windows，也是这样么？
答案：windows中有PCB，线程在内核中也需要管理（先描述再组织）    windows 是在进程中创建 TCB 控制块
	  但是如果说进行多个控制块结构体等的维护，就会导致维护更为复杂，复用task_struct，用进程来模拟线程，
	  包括内核代码也复用！

	进程是整个内核数据结构 + 代码数据    其他线程也属于进程一部分！
	这样对于调度结构和调度算法不需要改变（只需要跟之前一样即可）


在Linux 角度：统一都叫做执行流！
在硬件CPU的视角：把统一的执行流叫做 ---> 轻量级进程！      Linux中类似win中的TCB线程概念统一叫做 --> 轻量级进程
				不需要区分task_struct 到底是线程还是进程了！
	CPU两件事：1.不断向系统触发时钟中断要求操作系统进行调度（主动）
			   2.调度的时候喂给操作系统什么就会执行什么！（被动）
	CPU调度：执行流 <= 之前的进程 


对于操作系统 和 具体的操作系统（Linux）怎么理解？
	答案：操作系统这门学科，只是提供思想；  
		  具体的操作系统，譬如：Linux 或者 win 会提供具体的实现思想和实现方案！
	对于之前的进程就是单线程！

	* 线程在进程地址空间内运行!

	进程是强调独占，部分共享（譬如：进程通信）
	线程强调共享，部分独占！


可执行程序就是文件，文件就在磁盘存储，可执行程序，存储的时候，天然就是4KB单位存储的！无论属性还是内容！

操作系统把物理内存也分为一个个的内存块！物理内存被操作系统划分为一块块4KB的内存块！
物理内存和磁盘中数据以4KB为单位进行IO交换的！

4KB就是基于文件系统的的数据块交换！
这个 4KB 存储空间 叫 页框 或者 页帧！


4GB/4KB = 104 8576
4KB划分，是操作系统划分的！不管是磁盘（文件系统）


操作系统需要管理页框的4KB，先描述，再组织！

struct page  里面并没有 虚拟地址 或者 物理地址！   只是有许多标志位，还有许多链接字段！


struct page mem[1048576];       
	这是一个数组，转换成为对数组的操作！
	管理一个内存，把一个个page描述起来，再定义数组！就会转换成为对数组的增删改查（对数组的操作）
	因为是数组是具有下标的！连续在物理内存中存放，只需要知道每一个下标，每一个page的起始物理地址，就知晓了！
		下标  index * 4KB ---> 就是对应的page对应的起始物理地址！
		具体的物理地址 = 起始物理地址 + 页内（4KB）偏移
结论：·不用在Page里面保存该page的起始地址，访问可以只访问一个两个字节，但是操作系统申请都是申请4KB的！
	  ·写时拷贝，定义一个全局变量（子进程一写  全局变量地址变了，虚拟地址没变，地址的值不同，就是发生写时拷贝了）
	  ·写时拷贝，变量一定在某个页中，当这个变量要发生写时拷贝，并不是该变量单个变量进行筛选，而是把整个变量所
	  对应的页，整体进行写时拷贝！
	
	这也就是之前在申请共享内存的时候必须是4KB的整数倍，因为操作系统申请共享内存，内存块也是以4KB为单位的！
  当可执行程序要进行缺页中断（要把外设数据载入内存），本质就是在该数组中找到一个flag为0的，没有人使用，就
  能把文件的配置内存，4KB加载进来
	所以ELF可执行程序要把section加载成为secment，就是把section合并成为一个一个4KB的secment，方便加载

	问题：申请物理内存，是在干什么？
	答案：查数组，改page！（改为已占用）
		  想要快速找到 ----> 建立内核数据结构的对应关系！


	申请内存的载体是 进程/线程，可以通过文件找到对应的文件缓冲区！
 打开文件就会有对应的文件缓冲区，file有address_space指针，地址空间，以树状形式！
注：页表绝对不能是单张页表！


页表 岔不开 虚拟地址
	虚拟地址，32位的数字，前10个比特位是一个整体，中间10个比特位是一个整体，后面12位是一个整体！
前十个 [0,1023],中间十个[0,1023],后面十二个[0,4095]
	页目录：对应前十个，对应下标[1,1024]    最多1024个页表项，页表项指向的是一个页表（下一级页表的地址）
	页表：存的是页的地址(填的物理地址)
页表实际是  虚拟地址到页框的映射

页表查找过程（是由MMU硬件转换自动完成）：
查找页表的第一阶段：查找到虚拟地址对应的页框！（理论上页目录只有一张，页表可以有多个：1024个，页表可以有1024个页
					表项是 4字节 ，对应单张页表的大小就是   4字节 * 1024  也就是4KB  一页）
页表查找的第二阶段：根据虚拟地址低十二位，作为页内偏移，访问具体字节！

	
	
	虚拟地址访问的是具体一个字节！会拿着一字节的低12位作为索引，就能找到任意一页内的任意字节了！
·有了需要访问的地址的起始页框地址 + 页内偏移（对应的内存都是以4KB进行申请，虚拟地址访问的时候低十二位充当页内
偏移，进行对应的某一个字节访问！）

虚拟地址 ---> 虚拟地址空间提供（给内核  给用户）
CPU中有CR3，CR3是当前进程的硬件上下文！CR3指向页目录，页目录的额物理地址被CR3寄存器指向！
这样CPU就能直接连接地址总线了！

	进CPU的是虚拟地址，从CPU出来的是物理地址


	问题：如果查找页表查找失败（给虚拟地址，查找页目录，但是发现页目录所对应的页表不存在），但是页表是合法的！
为什么说他是合法地址？
	因为进程在地址空间里是合法的！代码和数据还没有加载到内存，发生写时拷贝！操作系统发生虚拟物理地址的转化失败，
操作系统内部发生中断，操作系统执行页面调度算法等，申请内存！就会访问描述内存的数据结构！发现内存块没有被占用！
·缺页中断交给PageFaultHandler处理，根据缺页中断不同类型会进行不同处理！（内存申请，构建数据结构）


（*）几大细节
细节一：申请内存 --> 查找数组 --> 找到没有被使用的page --> page index --> 物理页框地址！
细节二：写时拷贝，缺页中断，内存申请等，背后都可能要重新建立新的页表和建立映射关系的操作！
	（页表中没有具体保存任何一个字节的虚拟地址，也没有保存任何一个字节的物理地址，页表里面用虚拟地址是充当页目录
	和页表的数组下标）
细节三：进程，一张页目录 + n张页表构建的映射体系，虚拟地址是索引，物理地址页框是目标，虚拟地址（低12位） + 页框
		地址 === 物理地址
细节四：问题：为什么虚拟地址是低12位，为什么是12这个数字，为什么是低不是高？
		答案：因为页框的大小是4KB[0,4095]（为什么是12这个数字）, 低12位连续的一定是属于同一个4KB的，前20位一定
			  是一样的！后12位不一样但是一定是属于同一个4KB，低12作为页的偏移，高20位区分页（为什么是低，高二十
			  位一样的时候，就是同一页！）

结论1：进程在使用内存的时候，只使用物理内存，一个进程只会使用一部分内存，对应的页表大小 << 1024 个（远远小于）
结论2：执行流看到的资源本质是  在合法的情况下，拥有多少虚拟地址，虚拟地址是资源的代表
	   虚拟地址空间 mm_struct + vm_area_struct 本质：进行资源的统计数据和整体数据，页表是一张虚拟到物理的地图

资源划分：本质就是地址空间划分
资源共享：本质就是虚拟地址的共享




线程
	线程进行资源划分：本质是划分地址空间，获得一定范围的合法虚拟地址；再本质，就是在划分页表！
	线程进行资源共享：本质是对地址空间的共享，再本质，就是对页表条目的共享


虚拟地址到物理地址转化失败就会缺页中断，发生写时拷贝，怎么得知当前页要写时拷贝？
答案：当系统访问当前内存页的时候，发现内存页权限位是r，fork之后权限位就由w改为r，r权限，并且这个页的引用计数不为
	  1，至少有其他进程也在引用，而且引入的区域是代码区，虚拟地址可以通过虚拟地址空间判断出来，代码区、数据区，
	  本来是可写，但是实际只是只读，引用计数还是大于1，所以就会写时拷贝；  
 
 如果判定缺页中断，映射关系没有建设完整，但是虚拟地址合法的，而且虚拟地址在地址空间上已经被申请了，在合法的范围
 内，但是页表的映射关系没有构建完整，也就要缺页中断，重新申请内存，重新构建映射关系！
 
怎么判断当前是非法操作呢？
答案：当前页映射关系已经有了，发现映射过去之后，权限不对，当前用户访问内核空间 / 读权限访问写，访问只读空间但是
	  做了一个写操作，就直接中断！

在使用物理地址的时候是离散的！但是与虚拟内存对应的线性地址是连续的！


·在物理地址中，把低12位全部清零，就知晓了页框号，再除以4KB，下标就知晓了，在全局数组中就能反向找到该页的struct
配置，struct配置中，本身就包含了页框的起始虚拟地址，加上刚清零的低12位，反向就能转向成为虚拟地址
（这个转化实际是由硬件MMU进行转化）
	为了减少内存消耗，加入了多级页表（多级页表会导致效率降低），为了解决效率低下，引入了TLB



缺页中断的不同类型：
	1.Hard Page Fault ---> Major Page Fault，硬缺页错误/主要缺页错误，物理内存中没有对应的物理页，要CPU打开磁盘
设备读取到物理内存中，再让MMU建立虚拟地址和物理地址的映射
	2.Soft Page Fault ---> Minor Page Fault,软缺页错误/次要缺页错误，物理内存中存在对应物理页，只不过可能是其他
进程调入的，发生缺页异常的进程不知道，此时MMU只需建立映射即可，不用从磁盘读取写入内存，一般出现在多进程共享内存
区域
	3.Invalid Page Fault 无效缺页错误，例如：进程访问的内存地址越界访问，又比如对空指针解引用内核就会报
segment fault 错误中断进程直接挂掉


问题1：怎么理解new & malloc？
答：new/malloc 并没有在物理内存上开辟空间，底层是系统调用，brk/mmap；底层只需要更改底层堆空间的范围，改的是虚拟
	地址，物理地址没有申请！new/malloc申请结束虚拟地址，并不一定会立刻使用堆空间，也就不用立刻申请物理空间，当用
	虚拟地址访问的时候，系统就会触发缺页中断！然后做内存的二次申请，本质是对物理地址的延迟申请！（延迟到真正
	需要使用的时候)，申请完先不用，就可先给别的进程使用，可以提高内存使用充分度！

问题2：怎么理解写时拷贝？
答：当fork之后，全局变量，父子进程访问指向同一张页框，全局变量所在的页框权限就会直接被设置成为只读的！因为页表
	映射只映射到页框！权限管理页框为单位，写时拷贝实际是对整个页框进行写时拷贝！

问题3：申请内存，在干啥？
答：只是申请地址空间就可以了，计数器的start，end指针进行前后移动，扩大一些即可！

	越界的程序，不一定会崩溃报错！这是因为越界，操作系统并不知道！需要自行调试！

问题4：如何区分缺页了？还是说是真的越界了？
答：（1）页号合法性检查：操作系统在处理中断或异常时，首先检查触发事件的虚拟地址的页号是否合法，页号合法但是
		 页面不在内存中，则缺页中断，如果页号非法，则越界访问
	（2）内存映射检查：操作系统还可以检查触发事件的虚拟地址是否在当前进程的内存映射范围内，如果地址在映射范围内
		 但页面不在内存中，则为缺页中断！如果地址不在映射范围内，则为越界访问！

	·线程资源划分的真相：只要将虚拟地址空间进行划分，进程资源就天然被划分好了！




线程优点
	1.创建一个线程代价比创建一个进程小很多！（只需要创建结束进行调度就可以）
	2.线程占用资源比进程少
	3.能充分利用多处理器的可并行数量
	4.等待慢速IO操作结束同时，程序可以执行其他计算任务
	5.计算密集型应用，为了能在多处理器系统上运行，将计算分解到多个线程实现
	6.IO密集型应用，为了提高性能，将IO操作重叠，线程可以同时等待不同的IO操作！
  ·7.与进程之间的切换相⽐，线程之间的切换需要操作系统做的工作要少很多
		  最主要的区别是线程的切换虚拟内存空间依然是相同的，但是进程切换是不同的。这两种上
		下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能
		损耗是将寄存器中的内容切换出。
		  另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说，一旦去切换上下
		文，处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚
		拟内存空间的时候，处理的页表缓冲 TLB （快表）会被全部刷新，这将导致内存的访问在一
		段时间内相当的低效。但是在线程的切换中，不会出现这个问题，当然还有硬件cache。
	
	线程不是越多越好，如果是计算密集型应用，并且CPU只有两个，那么最合理的线程个数就是2个，不宜太多（个数等同核数）
不然需要把时间分一部分给切换线程
	IO密集型，可以多创建线程

	进程切换：只需要把对应的硬件上下文切换完即可
	线程切换（同一进程内）：需要切换对应的task_struct,PCB在切换，但是对应的页表不用换！不需要保存CR3寄存器！
进程间切换会导致TLB和Cache失效，下次运行会重新刷新，单个线程切换不会导致缓存失效



线程的缺点
	1.性能损失
	  一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计
	算密集型线程的数量比可用的处理器多，那么可能会有较大的性能损失，这里的性能损失指
	的是增加了额外的同步和调度开销，而可用的资源不变。
	2.健壮性降低
	  编写多线程需要更全面更深入的考虑，在一个多线程程序里，因时间分配上的细微偏差或者
	因共享了不该共享的变量而造成不良影响的可能性是很大的，换句话说线程之间是缺乏保护的。
	3.缺乏访问控制
	  进程是访问控制的基本粒度，在一个线程中调用某些OS函数会对整个进程造成影响。
	4.编程难度提高
	  编写与调试⼀个多线程程序⽐单线程程序困难得多


线程异常
	1.单个线程如果出现除零，野指针问题导致线程崩溃，进程也会随着崩溃
	2.线程是进程的执行分支，线程出异常，就类似进程出异常，进而触发信号机制，终止进程，进程
	  终止，该进程内的所有线程也就随即退出


线程用途
	合理的使用多线程，能提高CPU密集型程序的执行效率
	合理的使用多线程，能提高IO密集型程序的用户体验（如生活中我们一边写代码一边下载开发工具，就是多线程
运行的一种表现）


进程和线程的区别
	1.进程是资源分配的基本单位
	2.线程是调度的基本单位
	3.线程共享进程数据，但也拥有自己的一部分数据:
	  ·线程ID
   （*）一组寄存器，线程要有自己的独立的上下文数据（意思是线程可以被独立调度的）     重要
   （*）栈（每个线程要有自己的独立的栈结构，线程是一个动态的概念，有生命周期）       重要
	  ·errno
	  ·信号屏蔽字
	  ·调度优先级
	之前的单进程 --> 具有一个线程执行流的进程




Linux线程控制
	创建多线程 ---> 需要使用第三方库来创建多线程
	ps -aL      这个是查看进程       LWP：轻量级进程   light weight process
	会有PID和LWP，也就是task_struct{ pid_t pid;  pid_t lwp; }     也就是刚创建两个一样，一旦开了新线程，pid设为
一样的（两个线程），LWP加一
	CPU进行调度：是看LWP，Linux中都是轻量级进程，LWP
	
	·注：对于线程调度时间片问题：等分给不同的线程

	对于线程，可能会出现异常，出现异常之后？
	答：一旦异常，就会退出；一旦一个线程出现问题，都会导致整个进程都崩溃！（健壮性较低）

	对于两个线程同时执行，会导致消息同时打印，而信息会混杂在一起，这是因为显示器本质就是文件，两个线程访问时，
访问的都是同一个终端文件，向显示器打实际就是在向显示器文件进行写入

	


引入pthread库
	问：为什么会有一个库，库是什么？
	答案：Linux系统，不存在真正意义的线程，是使用轻量级进程模拟的，但OS中只有轻量级进程，所为模拟线程，只是说法！
也就是说：Linux提供系统调用，实际上也只是提供创建轻量级进程的系统调用！
	pid_t vfork(void);      和父进程共享地址空间（定义的全局变量，一旦修改，都可以看到，不会发生写时拷贝）

·对于用户来说，用户是只认识线程，而Linux是提供轻量级进程的系统调用！所以，引入了软件层，pthread库，把创建轻量级
  进程封装起来，给用户提供一批创建线程的接口！（引入之后，对于LWP，用户就不再需要）

	Linux的线程实现，是在用户层实现的：称之为用户级线程！
	pthread:原生线程库

	C++ 11 的多线程，在Linux下，本质是封装了pthread库！
	在windows下，封装windows创建线程的接口！
对于C++语言对这两种情况做了封装，实际就是为了保证语言的跨平台性和可移植性！

	跨平台的可移植性 ---> 所有平台都实现一份，然后条件编译，形成库！




pthread库相关操作
	Linux 线程控制的接口
	头文件  #include<pthread.h>
	   int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);
		 第一参数：pthread_t     线程id     输出型参数
		 第二参数：线程属性，nullptr（一般设为nullptr，默认）
		 第三参数：函数指针，回调机制，新线程要执行的函数入口
		 第四参数：设置为 nullptr 表示第三参数的函数不需要参数，如果设置，就是第三参数的参数（void*）
	
	·创建成功返回值就为0，不成功返回错误码，对应输出型参数thread就是未定义

	线程创好之后，新线程要被主线程等待，类似于僵尸进程的问题 ---> 内存泄漏

	等待子进程接口
	头文件   #include<pthread.h>
		int pthread_join(pthread_t thread,void **retval);
	
	对于输出流C语言
		printf(const char* format,...)   向显示器上进行格式化输出
		fprintf(FILE *stream,const char *format,...);        往指定的文件流里面进行输出
		sprintf(char* str,const char char* format,...);      几乎同printf，只是把对应的内容格式化输出到字符串中
		snprintf(char* str,size_t size,const char* format,...)   当前格式写到str指向的缓冲区里，大小为size
		
	获取线程id  
		pthread_t tid = pthread_self();       
	static_cast 类型转换   例如：std::string name = static_cast<const char*>(args);

·不管是新创建的线程还是，原来的主线程，都有自己的线程id，而且创建进程要进行对其等待，不等待就会出现类似僵尸进程
的情况！
·在线程领域，全局变量和函数，都是直接可以共享的！因为地址空间是共享的！公共资源！也就是多个执行流可以同时调用同
一个函数，这样的话，这个函数就是  被重入了！（用缓冲区（不是全局的是临时的） ---> 重入没有问题）
该函数 ----> 可重入函数！



线程传参和返回值
	对于之前的  
		func(int *a){ *a =100; }
		int main(){ int x = 0;  fun(&x); }   //这样才能拿到a的100   因为实际只是一份临时拷贝

	再者
		func(int *x){ x = 0x1234; }
		main(){ int *a = NULL; fun(a); }      这样是无法拿到a的
	
	因为a只是指针变量，a有存储空间，想要拿得到，就需要把func的参数传二级指针
		func(int **x){ *x = 0x1234; }
		main(){ int *a = NULL; func(&a); }    
	这样才能拿到x，因为若传一级指针，指针变量在形参实例化时，也要发生临时拷贝！ 


·申请完页表之后，进程所对应的页表会有一个数据结构来指向,mm_struct叫做地址空间，地址空间有一个pgd(页目录起始地址)
·所谓页表，就是进程在加载的时候，操作系统给申请的至少一个页目录，后面内容若需要，就动态申请页表，
  动态建立内存页,和页表之间的映射关系,页表中的各个条目-->无符号整数(可以把地址数据做保存，地址数据本质就是整数)

结论：
	对于多线程
		（1）main函数结束，代表主线程结束，一般也代表进程结束
		（2）新线程对应的入口函数运行结束，代表当前线程运行结束（void* routine(void *args)）
			 主线程的入口是main
		（3）给线程传递的参数和返回值，可以是任意类型（包括自定义的对象）


	对于线程的终止
		（1）线程的入口函数，进行return就是线程终止（推荐）
		 注意：线程是不能调用exit()终止的，因为exit是终止进程的！    进程终止
		（2）pthread_exit(),这个是终止线程的，终止调用线程
		（3）pthread_cancel()，主线程取消新线程，要求新线程已经启动了！    被取消了，线程退出结果是-1
	对应的退出接口
	·	#include<pthread.h>
	·	void pthread_exit(void *retval);                     return void*
	新线程中，return 一个值作为退出码和这个效果一样！！！

	·	#include<pthread.h>
	·	int pthread_cancel(pthread_t thread);
	这个是主线程取消新线程，对于取消的新线程，退出的结果是-1   [PTHREAD_CANCELED]

对于创建了新线程 ---> 主线程和新线程（不清楚谁先进行执行） 
pthread_join 拿到的返回值，就是线程退出设定的返回值！因为使用pthread_join默认线程无异常！

		
	问题：如果主线程不想再关心新线程，而是让新线程结束的时候，自己进行释放，怎么做？
	答案：可以设置线程为分离状态
		  对于技术层面：线程默认是需要被等待的！ 状态是 joinable，若不想让主线程等待新线程，想让其结束之后自己退，
		  需要把它状态设置为分离状态！（!joinable状态  或  detach） 
	·对于这个线程分离 --> 可以是主线程分离新线程，也可以是新线程把自己分离
	·分离的线程，依然在进程的地址空间中，进程的所有资源，被分离的线程，依旧可以访问，可以操作！只不过，主线程
  不再等待新线程！（把主线程从等待中解放出来）


  变成 分离线程   detach
	 pthread_detach(tid);        把线程分离   这个是主线程给新线程分离了
  自己给自己分离
	 pthread_detach(pthread_self());


Linux 是没有真正的线程，他是用轻量级进程模拟出来的！
	OS提供的接口，不会直接提供线程接口。在用户层，会封装轻量级进程，形成原生线程库！（创建和访问线程，本质都
	是访问线程级的库）
	libpthread.so.0 ==> /lib/x86_64-linux-gnu/libpthread.so.0()
	
	libpthread.so.0 ==> /lib/x86_64-linux-gnu/ 是可执行程序！！！ELF可执行程序！			      
	libpthread.so.0 动态库 -> ELF的库

·	我的可执行程序加载，形成进程，动态链接和动态地址重定向！   可执行程序是一个ELF的二进制文件，动态库也是
·	要将动态库加载到内存 && 映射到当前进程的地址空间中！！！
进程自己的代码区可以访问到pthread库内部的函数或者数据


·线程的概念是在库中维护的！在库内部，会存在多个被创建好的线程！库，是需要管理线程的！（先描述，再组织）
  在线程 pthread库内部，一定有着类似  struct tcb这样的结构体
  struct tcb								对于优先级、时间片、上下文 ---> LWP（PCB中的内容）
  {											也就是线程的一部分在内核中实现，一部分在用户层实现
	 //线程应该有的属性
		 线程状态
		 线程id
		 线程独立的栈结构
		 线程栈大小
	......
  }

  pthread库在内存中，struct_pthread  就是tcb
	对于mmap区域 --> 拥有struct pthread & 线程局部存储 & 线程栈
	整个线程数据块用一个数组进行管理   当给用户 pthread_create() 的那个返回值很大的那个数字 ---> 实际是线程在库
  中，对应的管理块的虚拟地址！也就是上面的多个内容组成的
	struct thread（tcb）   线程局部存储   线程栈  ----> 返回的就是组成块的起始地址
	tcb 中有一个属性 --> void *ret

（将来控制块，对应的线程代码执行结束，return的时候，就会把对应的返回值写到当前结构体struct pthread的指针ret里！
  运行结束，数据块并没有被释放，所以线程需要join！对于pthread_join的时候pthread_join(tid，&ret); 必须要传入tid，
  这个tid就是上一个退出的线程的管理块的起始地址！！！通过&ret，把对应void *ret的内容给带出来！）

总：自己代码区中调用 pthread_create 其实是在动态库内部帮我们创建该线程的管理块，管理块内部叫tcb，包含了线程
	相关的信息，tcb包含字段void *ret，当当前线程被运行的时候，此时结束会把返回值拷贝到线程控制块的void* ret中，
	因为主线程新线程共享地址空间，只要拿到起始虚拟地址，就可以找到退出线程的控制块！
	内存泄漏实际是（控制块中没释放）所以需要使用pthread_join(tid,&ret),从管理块中把对应信息拷贝回来！这也就是
	为什么需要二级指针！！ps查不到就是因为，线程结束时，OS 内核中LWP自动就被释放了！但是库中并未被释放！


·每一个线程，一定都有自己独立的栈空间！（主线程用地址空间里面的栈，新创建线程会用自己对应的线程栈）


问题：用户线程 和 LWP是如何联动的呢？
答：实际上，当我们创建线程时，pthread_create内部既帮我们创建数据块，又帮我们在内核里创建轻量级进程（调用系统调用，
	执行什么方法，栈在哪里）
	这个系统调用叫克隆
	int clone(int (*fn)(void*),void *stack,int flags,void *arg,...)  第一个是routine（用户方法），第二个是stack用
	户栈结构 
	然后线程执行的时候，执行的就是routine的方法，临时数据形成就会放在栈结构里，临时数据保存会在用户空间！


用户线程：只需要在库里建好描述线程的相关属性即可！剩下的给底层指明，需要执行什么方法，临时数据保存在数据块的
线程栈里面，在用户空间的属性就不用改变了！在用户看来，就有线程，但是真正执行的，是内核的轻量级进程！执行结束
会把结果写到struct pthread里面的 void *ret 里！

	Linux 是用户级线程 ：内核LWP = 1:1




几个概念
	1.线程ID
	pthread_create的时候，在库中创建的描述线程的控制块的起始虚拟地址

	2.线程传参和返回值
	线程执行结束，把线程的退出结果写入到线程控制块的对应void* 变量里，然后通过pthread_join拿到他！

	3.线程分离
	在线程控制块里，有一个线程状态  int joinable，表明当前线程是否是被分离的！默认情况下是1，线程结束必须
	join，不join的话就不会释放！detach就是把joinable置为0！识别到线程一旦在底层退出，结束之后识别到struct pthread
	属性里面，joinable的字段为0，线程库会自动把对应的内容释放掉！

pthread 中有个指针指向线程栈，动态库映射是在 0-3GB，可以被用户在代码区用代码直接访问的
pthread_join (tid,&ret)    传入tid实际就是找到对应的结构体

Linux 所有的线程，都在库中！
	
	我们自身就可以使用clone创建轻量级进程，pthread库就是对clone做了一下封装！在库中多创建了一个管理线程的
 线程块!内部申请一段空间，拿这块空间充当对应线程的独立栈结构！原生线程库就是对轻量级进程的封装，叫做线程！   



	·每一个线程都是有自己独立的栈结构的，都使用自己的线程栈！都在库中进行维护！

每一个线程都有自己独立的上下文，都有自己独立的栈结构！
	独立的上下文：有独立的PCB(内核) + TCB（用户层，pthread库内部）
	独立的栈：每个线程都有自己的栈，要么是进程自己的，要么就是库中创建进程时mmap申请出来的！
线程中所有的空间，其实事实上每一个线程都能够访问另外一个线程的任何资源！大部分都是共享的！



想要获取线程名
	1.int pthread_setname_np(pthread_t thread,const char *name);    参数1：线程id  参数2：设置的名字

	2.int pthread_getname_np(pthread_t thread,char *name,size_t len);   另一个函数中就能get，当然应是同一个线程
	  参数1：线程id     参数2：把name放哪     参数3：大小
*/